Skip to content

serve: name the HSA runtime HRX loads, path and sha256 (engine#713 criterion 4/5) - #888

Merged
bong-water-water-bong merged 4 commits into
mainfrom
1bit/713-name-hsa-runtime
Nov 3, 2026
Merged

bong-water-water-bong merged 4 commits into
mainfrom
1bit/713-name-hsa-runtime

Conversation

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator

The bootstrap itself landed in #771 (a3f64ec50); this PR closes the part of the issue's own acceptance list that was
still missing — criterion 4 (the HRX init line names the HSA runtime, path and sha256) and criterion 5 (the
release table carries the toolchain pin with that sha256 and the measured capability lines) — and records the
measurements for the rest.

closes #713

Why the runtime has to be named (criterion 4)

Three libhsa-runtime64.so.1 exist on a box and they are not interchangeable (the issue's residual table): the
release-shipped one does not register HRX0 for a dev build, the pinned toolchain's does, a distro one is
untested. Before this PR serve resolved the path (hrx_libhsa()) and handed it to llama-server but never said which
file it was, so a silent fallback could pass a run while a different runtime executed — which is exactly why the
engine#315 residual A/B was trustworthy: every arm named its runtime.

What changed

  • app/hrx_runtime.h (new, header only): the tree's one SHA-256 (FIPS 180-4) implementation — lifted from
    tests/npu_q4nx_test.cpp, which now includes it instead of carrying its own copy — plus sha256_file() and the line
    HRX: HSA runtime <path> sha256 <hex>, with … (unreadable: no sha256, HRX may fail to open it) when the file
    cannot be read (HRX still tries to dlopen it, so the log has to name it).
  • app/serve.cpp: announce_hrx_runtime() beside announce_cwsr_env() at all three HRX backend sites (chat
    server, role/RAG server, Laya daemon), once per process, before the CWSR decision line.
  • Tests, both without a GPU: tests/hrx_runtime_test.cpp (NIST vectors — empty, "abc", the 56-byte two-block
    message — a real file against its bytes, a missing file as "", both line forms) and tests/hrx_runtime_route.sh
    (end to end: a fake runtime file through serve, so the line names it with sha256sum's digest exactly once, the
    backend's IREE_HAL_AMDGPU_LIBHSA_PATH is that same path, an unreadable path is named without a fabricated digest,
    and --device cpu prints no runtime line at all).
  • Docs: docs/hrx.md "The HSA runtime" gained the line and how to check it; docs/releases.md gained the pinned
    runtime's size/sha256 and its measured capability lines in a table beside the pins (criterion 5).

Measured on Strix Halo (RYZEN AI MAX+ 395 / Radeon 8060S, gfx1151)

Box state for every row: origin/main (23d6972) with its pinned submodules, the pinned toolchain fetched into
third_party/toolchain/rocm-sdk-10.3.0a20261031-gfx1151
, /opt/rocm-therock made an empty tmpfs inside a private
mount namespace
(so nothing on the box changed and no run could use it), no ROCM_PATH/HIP_PATH/
HIP_DEVICE_LIB_PATH/-DONEBIT_HRX_TOOLCHAIN.

criterion result
7 (build from the repo alone) configure: ONEBIT_HRX: toolchain …/third_party/toolchain/rocm-sdk-10.3.0a20261031-gfx1151, and the same copy for the HSA runtime and the amdgcn device library; cmake --build --target onebit llama_hrx rc 0, test-backend-ops rc 0
1 (HRX0 up on a dev build) 1bit serve -m Qwen3-0.6B-Q4_K_M.gguf --device hrx (defaults, no libhsa override) answered the warm-up and a completion with HTTP 200; decode 256 tok/s, prefill 73 tok/s
4 (runtime named at init) 1bit serve: HRX: HSA runtime …/rocm-sdk-10.3.0a20261031-gfx1151/…/_rocm_sdk_core/lib/libhsa-runtime64.so.1 sha256 21a6682452e159cfcdc6bdc84676f6b7f9c1236f3a2957a97a2e7170889cdfa3 — identical to sha256sum of that path (5,141,585 B), the digest the issue's residual table lists for TheRock's nightly
2 (the device is exercised) test-backend-ops test -b HRX0 -o TOP_K: 484/484 tests passed, Backend HRX0: OK, 2/2 backends passed, zero fault lines
3 (no new faults in the shipped configuration) gfxhub … page fault count in dmesg 0 before, 0 after both runs above
6 (preflight without /opt/rocm-therock) /opt hidden as above: scripts/weekly.sh <W> build gets past preflight and resolves hipcc from the tree copy (HIP version 7.18.26434-0000000, AMD clang 24.0.0git). With the bootstrap unable to complete (no wheel index): preflight: cannot fetch the pinned HRX toolchain (config/hrx-toolchain.config) then preflight failed: nothing was run, rc 1 — no fallback to a box-local runtime. scripts/fetch-rocm-sdk.sh --check is rc 0 at the pin, rc 1 for a missing tree and for a tree at another pin
5 (release table) docs/releases.md: the toolchain row carries the runtime digest, and a table below it records 5,141,585 B / 21a66824… and XNACK enabled: NO, Coherent Host Access: FALSE, Memory Properties: APU, Runtime Version 1.22 (measured with the pinned tree's own rocminfo, /opt hidden)
tests `ctest -R 'hrx_runtime

Not measured: criterion 7's byte-for-byte comparison (llama-server --version and the embedded ggml commit) against a
second build of the same tree made with -DONEBIT_HRX_TOOLCHAIN=/opt/rocm-therock. The build was made and run from the
repository alone, which is the substance of the criterion; the diff of the two binaries is a follow-up.

Notes

  • The completion body above is empty with finish_reason: "length" for Qwen3-0.6B: this model's chat template puts the
    answer in reasoning_content and serve's chat-format parser rejects it in the warm-up (a pre-existing template quirk,
    unrelated to this change); the HTTP 200 and the JSON envelope are what criterion 1 asks for.
  • The repository has no CHANGES/CHANGELOG file, so there is no changelog entry to add.

The engine#713 bootstrap (the pinned toolchain under the tree) landed in #771. What was still
missing from that issue's acceptance list is criterion 4: HRX resolved the libhsa-runtime64.so.1 it
would dlopen and handed it to llama-server, but never said which file it was. Three exist on a box
and they are not interchangeable - the release-shipped one does not register HRX0 for a dev build -
so a run could silently fall back to another copy.

app/hrx_runtime.h carries the tree's one SHA-256 implementation (tests/npu_q4nx_test.cpp now shares
it instead of keeping its own), the file digest, and the line; serve prints it once at the first HRX
backend, before the CWSR decision line, and docs/releases.md records the pinned runtime's sha256 and
its measured gfx1151 capability lines next to the toolchain pin (criterion 5).

Tests, both without a GPU: tests/hrx_runtime (NIST vectors, a real file against its bytes, a missing
file, both line forms) and tests/hrx_runtime_route (a fake runtime file through serve: the line and
its digest checked with sha256sum, the IREE_HAL_AMDGPU_LIBHSA_PATH the backend receives, an
unreadable path named without a fabricated digest, and --device cpu printing nothing).
@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

#888 ready for review at f1605c2

@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

PR Reviewer Guide 🔍

(Review updated until commit c507e90)

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Runtime Naming Consistency

The announce_hrx_runtime function is called in three different HRX backend paths, but there's a potential inconsistency in when the runtime is announced. Specifically, in the laya_daemon section (lines 1716-1719), the runtime is announced only when dev is null, empty, or "hrx", but not in the other two paths. This could lead to the runtime not being announced in some cases where it should be, potentially causing confusion in logs or debugging.

if (!dev || !*dev || std::string(dev) == "hrx") {
    announce_hrx_runtime(o);
    announce_cwsr_env(o);
}
SHA-256 Implementation

The SHA-256 implementation in hrx_runtime.h is a full inline implementation. While this ensures portability, it may not be as performant as a library implementation. However, since this is a header-only file and used for logging purposes, the performance difference is likely negligible. The implementation is correct and matches FIPS 180-4.

inline std::string sha256_hex(const uint8_t* data, size_t len) {
    static const uint32_t K[64] = {
        0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
        0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
        0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
        0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
        0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
        0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
        0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
        0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2};
    uint32_t h[8] = {0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19};
    std::vector<uint8_t> m(data, data + len);
    m.push_back(0x80);
    while (m.size() % 64 != 56) m.push_back(0);
    for (int i = 7; i >= 0; i--) m.push_back(uint8_t((uint64_t(len) * 8) >> (8 * i)));
    auto rotr = [](uint32_t x, int n) { return (x >> n) | (x << (32 - n)); };
    for (size_t off = 0; off < m.size(); off += 64) {
        uint32_t w[64];
        for (int i = 0; i < 16; i++)
            w[i] = uint32_t(m[off + 4 * i]) << 24 | uint32_t(m[off + 4 * i + 1]) << 16 |
                   uint32_t(m[off + 4 * i + 2]) << 8 | m[off + 4 * i + 3];
        for (int i = 16; i < 64; i++) {
            const uint32_t s0 = rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ (w[i - 15] >> 3);
            const uint32_t s1 = rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ (w[i - 2] >> 10);
            w[i] = w[i - 16] + s0 + w[i - 7] + s1;
        }
        uint32_t a = h[0], b = h[1], c = h[2], d = h[3], e = h[4], f = h[5], g = h[6], hh = h[7];
        for (int i = 0; i < 64; i++) {
            const uint32_t t1 = hh + (rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25)) + ((e & f) ^ (~e & g)) + K[i] + w[i];
            const uint32_t t2 = (rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) + ((a & b) ^ (a & c) ^ (b & c));
            hh = g; g = f; f = e; e = d + t1; d = c; c = b; b = a; a = t1 + t2;
        }
        h[0] += a; h[1] += b; h[2] += c; h[3] += d; h[4] += e; h[5] += f; h[6] += g; h[7] += hh;
    }
    char hex[65];
    for (int i = 0; i < 8; i++) std::snprintf(hex + 8 * i, 9, "%08x", h[i]);
    return hex;
}

The CI build lists every target a ctest test invokes (the comment above it says why: ctest runs
whatever binaries exist). engine#713's new device-free test was missing from the list, so the first
run of PR #888 failed with "32 - hrx_runtime (Not Run)".
@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

Thanks — two answers, and the first one is a real (if cosmetic) point.

static bool announced is not thread-safe. Correct in general, and it deliberately matches the neighbour it sits next to: announce_cwsr_env (app/serve.cpp:253-257) uses the identical pattern for the identical reason, and all three call sites are on serve's single-threaded launch/setup path — a lost or duplicated line here is a cosmetic outcome, not a wrong runtime (the path is hashed from the same hrx_libhsa() call the backend env gets). Converting both to a std::call_once/atomic would be the right cleanup; I did not do it in this PR so that it stays a two-line change and does not diverge from the CWSR line's shape. Happy to make it a follow-up if you prefer consistency the other way.

SHA-256 in a hot path: it runs once per serve process, at the first HRX backend, over a ~5 MB .so. Measured with this header, -O2: 24 ms for the pinned 5,141,585 B libhsa-runtime64.so.1 (digest 21a66824…, the same one the init line prints). No optimisation needed; the trade is ~1/40 of a second against being able to tell which of three runtimes a run used.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

#888 ready for review at ebacdfa

(supersedes f1605c2: ci.yml now builds hrx_runtime_test — CI's build step lists every target a ctest test invokes, and mine was missing, so the first run reported "32 - hrx_runtime (Not Run)". Pushed over SSH because the gh OAuth token has no workflow scope.)

@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown

Persistent review updated to latest commit ebacdfa

1 similar comment
@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown

Persistent review updated to latest commit ebacdfa

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: READY

At ebacdfa6e (BEHIND main by merges only, no conflict). Built and ran everything the PR claims, plus the arithmetic.

Built and run at this head (cmake -DCMAKE_BUILD_TYPE=Release, targets onebit hrx_runtime_test):

./hrx_runtime_test                        -> hrx_runtime: all checks passed
ctest -R '^hrx_runtime$'                  -> 1/1 Passed (registered, not Not Run)
tests/hrx_runtime_route.sh ./build/1bit   -> 8/8 ok, rc 0
   ok  the backend gets the runtime serve named
   ok  serve names the runtime and its sha256
   ok  the digest is the file's, checked with sha256sum
   ok  announced exactly once
   ok  missing: the line names the path and says it is unreadable
   ok  missing: no fabricated digest
   ok  --device cpu: no runtime line
   ok  --device cpu: no IREE_HAL_AMDGPU_LIBHSA_PATH in the backend's env

The SHA-256 against coreutils, on inputs bigger than the FIPS vectors. The vectors pin single-block and 56-byte cases; a long-length encoding bug would survive them, and this digest is now printed on every HRX serve, so I hashed real files through onebit::sha256_file() and compared with sha256sum:

21a6682452e159cfcdc6bdc84676f6b7f9c1236f3a2957a97a2e7170889cdfa3  libhsa-runtime64.so.1   (5,141,585 B)   both agree
13e33455f91cca68760e18e7f1ccff1455834ca2065a9c36f802e6e627ba6af5  Qwen3-4B model.onnx     (2.2 GB)        both agree

The pin in docs/releases.md is the real file, and the three-runtime premise is not hypothetical — every copy on this box:

5,141,585  f5462dc36b0a…  release-rc/work/os/data.dir/1bit-llama/libhsa-runtime64.so.1   (same size, different bytes)
5,141,585  21a6682452e1…  rocm-sdk-10.3.0a20261031-gfx1151/…/_rocm_sdk_core/lib/…       = the documented pin
4,957,049  f6ef689d163b…  release-shipped /opt/rocm-therock/lib/…                        (c16 squashfs skeleton)

The documented size and full digest match the pinned tree byte for byte, and there really are three files called the same thing that are not the same file.

The capability row, reproduced with the pinned tree's own rocminfo under gpu-hold -l "pi prs 888" -t 20: Runtime Version 1.22, System Timestamp Freq.: 1000.000000MHz, XNACK enabled: NO, Coherent Host Access: FALSE, Memory Properties: APU, gfx1151 / AMD Radeon 8060S Graphics. Every field in the new table is what the tool prints.

Checked by reading, since they decide when the line appears: all three call sites are HRX-only (launch_for under device == "hrx", rag_launch under the same, serve_child under !dev || dev == "hrx"), the line comes from the same hrx_libhsa() call that fills IREE_HAL_AMDGPU_LIBHSA_PATH, and serve_child is called in-process (app/serve.cpp:2970), so the static bool guard means once per process rather than once per fork. CI's explicit target list gains hrx_runtime_test (ci.yml:77), which is the #635 trap and it is handled.

One nit, worth one line: sha256_hex() does std::vector<uint8_t> m(data, data + len), so the empty-file case (sha256_file() passes bytes.data(), which may be null for an empty vector) forms nullptr + 0. It returns the right digest on libstdc++ and the runtime is never empty, but an early if (!len) return "e3b0c442…"; or a one-line guard keeps a UBSan run quiet.

@bong-water-water-bong bong-water-water-bong added the tprs-ready TPRS: review verdict READY posted by the standing reviewer; merge gate satisfied label Nov 3, 2026
@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

#888 ready for review at e39b3fb

(ebacdfa6 was 9 commits behind main, so the branch now carries origin/main as a merge — no rebase, no
conflict, and the PR's own two commits are unchanged. Reason: this PR is on the c21 cut list, and a head that is
BEHIND would have needed the waiter or the owner to do the merge anyway.)

What I re-checked at the merged head:

  • merge is textually clean (git merge-tree → 0 conflict hunks) and both submodule gitlinks are still the
    fleet's pins — third_party/hrx-system 82804197b, third_party/llama.cpp d8bd0ff6052a.
  • the one real overlap is docs/hrx.md, and it does not collide: hrx: hrx-env names the HSA runtime, and a parity row compares CPU and device output (engine#674) #883 (now in main) added the
    trio/IREE_HAL_AMDGPU_LIBHSA_PATH paragraph in "The trio belongs to every HRX process"; build: the engine should fetch and pin its own HRX toolchain instead of requiring /opt/rocm-therock on the box #713's bullet is in the
    CMake "The HSA runtime" list. Same subject from the two ends (the recipe fills the path; serve names the file it
    actually opened and its sha256), no duplicated claim — I re-read both regions rather than trusting the clean merge.
  • tools/copyright.py --check rc 0 (main also rewrote tools/copyright.py, so this was worth re-running).
  • incremental build at the merged head: rc 0, and ctest -R 'hrx_runtime|hrx_cwsr|print_backend|mem_admit|npu_q4nx' → 8/8 pass (the two new tests plus the four
    regressions the extra log line could have broken). Under thermal-run, -j4; full CI is running on this head now.

@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown

Persistent review updated to latest commit e39b3fb

@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown

Persistent review updated to latest commit c507e90

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: READY at c507e90ed — a merge of eight main commits into the head I verdicted. Seven of this PR's nine files are blob-identical; CMakeLists.txt and docs/hrx.md differ, and the difference is main's, not this PR's: hrx_cpu_weight_parity + the hrx-env.sh IREE_HAL_AMDGPU_LIBHSA_PATH recipe (#883) and npu_window11_policy (#880). Every hunk this PR adds is still present at the new head — add_executable(hrx_runtime_test), add_test(NAME hrx_runtime …), hrx_runtime_route, the documented pin 21a6682452e159cf… in docs/releases.md, and announce_hrx_runtime's four call-path references — and the PR's own diff is the same nine files, +339/−41.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

#888 ready for review at c507e90

The waiter merged main (#885) into the branch itself, so this is a re-ping on the head it produced rather than a new
change from me. Verified at c507e90e:

  • all checks green: build pass (5 m 11 s), pr_agent pass (59 s), Analyze ×4 pass, CodeQL pass, hw_gate
    skipping by design; mergeStateStatus: CLEAN, mergeable: MERGEABLE.
  • the waiter's merge kept this PR's work intact: git diff e39b3fb..c507e90e is npu forward: read the batched prefill's GEMM outputs at the kernel's N stride (engine#855 part 2) #885's NPU files only (6 files,
    +377/−33), and app/hrx_runtime.h, tests/hrx_runtime_test.cpp, tests/hrx_runtime_route.sh are all present.
  • my own local verification stands from the earlier comment (ctest -R 'hrx_runtime|hrx_cwsr|print_backend|mem_admit|npu_q4nx'
    → 8/8; tools/copyright.py --check rc 0; submodule gitlinks still hrx-system 82804197b, llama.cpp d8bd0ff6052a).

The earlier pr_agent red was its own concurrency cancel (conclusion: cancelled, empty step list), not a finding; it
passed on re-run and then passed again on this head.

@bong-water-water-bong
bong-water-water-bong merged commit 18e74a5 into main Nov 3, 2026
8 checks passed
@bong-water-water-bong
bong-water-water-bong deleted the 1bit/713-name-hsa-runtime branch November 3, 2026 12:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Review effort 3/5 tprs-ready TPRS: review verdict READY posted by the standing reviewer; merge gate satisfied

Projects

None yet

Development

Successfully merging this pull request may close these issues.

build: the engine should fetch and pin its own HRX toolchain instead of requiring /opt/rocm-therock on the box

1 participant